{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Serving XGBoost models\n",
    "\n",
    "Out of the box, `mlserver` supports the deployment and serving of `xgboost` models.\n",
    "By default, it will assume that these models have been [serialised using the `bst.save_model()` method](https://xgboost.readthedocs.io/en/latest/tutorials/saving_model.html).\n",
    "\n",
    "In this example, we will cover how we can train and serialise a simple model, to then serve it using `mlserver`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Training\n",
    "\n",
    "The first step will be to train a simple `xgboost` model.\n",
    "For that, we will use the [mushrooms example from the `xgboost` Getting Started guide](https://xgboost.readthedocs.io/en/latest/get_started.html#python)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Original code and extra details can be found in:\n",
    "# https://xgboost.readthedocs.io/en/latest/get_started.html#python\n",
    "\n",
    "import os\n",
    "import xgboost as xgb\n",
    "import requests\n",
    "\n",
    "from urllib.parse import urlparse\n",
    "from sklearn.datasets import load_svmlight_file\n",
    "\n",
    "\n",
    "TRAIN_DATASET_URL = 'https://raw.githubusercontent.com/dmlc/xgboost/master/demo/data/agaricus.txt.train'\n",
    "TEST_DATASET_URL = 'https://raw.githubusercontent.com/dmlc/xgboost/master/demo/data/agaricus.txt.test'\n",
    "\n",
    "\n",
    "def _download_file(url: str) -> str:\n",
    "    parsed = urlparse(url)\n",
    "    file_name = os.path.basename(parsed.path)\n",
    "    file_path = os.path.join(os.getcwd(), file_name)\n",
    "    \n",
    "    res = requests.get(url)\n",
    "    \n",
    "    with open(file_path, 'wb') as file:\n",
    "        file.write(res.content)\n",
    "    \n",
    "    return file_path\n",
    "\n",
    "train_dataset_path = _download_file(TRAIN_DATASET_URL)\n",
    "test_dataset_path = _download_file(TEST_DATASET_URL)\n",
    "\n",
    "# NOTE: Workaround to load SVMLight files from the XGBoost example\n",
    "X_train, y_train = load_svmlight_file(train_dataset_path)\n",
    "X_test, y_test = load_svmlight_file(test_dataset_path)\n",
    "\n",
    "# read in data\n",
    "dtrain = xgb.DMatrix(data=X_train, label=y_train)\n",
    "\n",
    "# specify parameters via map\n",
    "param = {'max_depth':2, 'eta':1, 'objective':'binary:logistic' }\n",
    "num_round = 2\n",
    "bst = xgb.train(param, dtrain, num_round)\n",
    "\n",
    "bst"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Saving our trained model\n",
    "\n",
    "To save our trained model, we will serialise it using `bst.save_model()` and the JSON format.\n",
    "This is the [approach by the XGBoost project](https://xgboost.readthedocs.io/en/latest/tutorials/saving_model.html).\n",
    "\n",
    "Our model will be persisted as a file named `mushroom-xgboost.json`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model_file_name = 'mushroom-xgboost.json'\n",
    "bst.save_model(model_file_name)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Serving\n",
    "\n",
    "Now that we have trained and saved our model, the next step will be to serve it using `mlserver`. \n",
    "For that, we will need to create 2 configuration files: \n",
    "\n",
    "- `settings.json`: holds the configuration of our server (e.g. ports, log level, etc.).\n",
    "- `model-settings.json`: holds the configuration of our model (e.g. input type, runtime to use, etc.)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### `settings.json`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%writefile settings.json\n",
    "{\n",
    "    \"debug\": \"true\"\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### `model-settings.json`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%writefile model-settings.json\n",
    "{\n",
    "    \"name\": \"mushroom-xgboost\",\n",
    "    \"implementation\": \"mlserver_xgboost.XGBoostModel\",\n",
    "    \"parameters\": {\n",
    "        \"uri\": \"./mushroom-xgboost.json\",\n",
    "        \"version\": \"v0.1.0\"\n",
    "    }\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Start serving our model\n",
    "\n",
    "Now that we have our config in-place, we can start the server by running `mlserver start .`. This needs to either be ran from the same directory where our config files are or pointing to the folder where they are.\n",
    "\n",
    "```shell\n",
    "mlserver start .\n",
    "```\n",
    "\n",
    "Since this command will start the server and block the terminal, waiting for requests, this will need to be ran in the background on a separate terminal."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Send test inference request\n",
    "\n",
    "We now have our model being served by `mlserver`.\n",
    "To make sure that everything is working as expected, let's send a request from our test set.\n",
    "\n",
    "For that, we can use the Python types that `mlserver` provides out of box, or we can build our request manually."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import requests\n",
    "\n",
    "x_0 = X_test[0:1]\n",
    "inference_request = {\n",
    "    \"inputs\": [\n",
    "        {\n",
    "          \"name\": \"predict\",\n",
    "          \"shape\": x_0.shape,\n",
    "          \"datatype\": \"FP32\",\n",
    "          \"data\": x_0.toarray().tolist()\n",
    "        }\n",
    "    ]\n",
    "}\n",
    "\n",
    "endpoint = \"http://localhost:8080/v2/models/mushroom-xgboost/versions/v0.1.0/infer\"\n",
    "response = requests.post(endpoint, json=inference_request)\n",
    "\n",
    "response.json()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As we can see above, the model predicted the input as close to `0`, which matches what's on the test set."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_test[0]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
